package de.klingbeil.hhc.testhelper;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

import javax.persistence.EntityManager;

import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.util.ReflectionUtils;

import com.google.gwt.event.shared.SimpleEventBus;
import com.google.inject.Provider;
import com.google.web.bindery.requestfactory.server.ServiceLayer;
import com.google.web.bindery.requestfactory.server.ServiceLayerDecorator;
import com.google.web.bindery.requestfactory.server.SimpleRequestProcessor;
import com.google.web.bindery.requestfactory.server.testing.InProcessRequestTransport;
import com.google.web.bindery.requestfactory.shared.Locator;
import com.google.web.bindery.requestfactory.shared.Receiver;
import com.google.web.bindery.requestfactory.shared.Request;
import com.google.web.bindery.requestfactory.shared.RequestContext;
import com.google.web.bindery.requestfactory.shared.RequestFactory;
import com.google.web.bindery.requestfactory.shared.ServiceLocator;
import com.google.web.bindery.requestfactory.vm.RequestFactorySource;

import de.klingbeil.hhc.testhelper.impl.MockingServiceLocator;

/**
 * Initializes a {@link RequestFactory} for JUnit tests. There are some hacks
 * involved for the following reasons:
 * <ul>
 * <li>
 * GWT searches the {@link com.google.web.bindery.requestfactory.shared.Locator}
 * for services using the annotation at the entity proxy. To ease testing, a
 * custom service locator returns mocked service instances that can be retrieved
 * for tests using {@link #getServiceMock(Class)}.</li>
 * <li>
 * {@link ServiceLayer} internally creates a ServiceLayerCache which has a
 * <b>static</b> cache for locators, services, etc. Hence, tests must always
 * call {@link #getServiceMock(Class)} to get a mocked service instance. This
 * class ensures that the cached instance is returned.</li>
 * </ul>
 * 
 * @see #createFactory(Class)
 * @see #getServiceMock(Class)
 */
@SuppressWarnings("unchecked")
public final class RequestFactoryHelper {

	private static class MockedServiceLayer extends ServiceLayerDecorator {

		@Override
		public <T extends ServiceLocator> T createServiceLocator(Class<T> clazz) {
			return (T) mockingServiceLocator;
		}

		@Override
		public <T extends Locator<?, ?>> T createLocator(Class<T> clazz) {
			return createInstance(clazz, entityManagerMock);
		}

		private <T> T createInstance(Class<T> clazz, EntityManager entityManager) {
			try {
				Constructor<T> ctor = clazz.getConstructor(EntityManager.class);
				return ctor.newInstance(entityManager);
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}

	private static final ServiceLocator mockingServiceLocator = new MockingServiceLocator();
	private static final SimpleRequestProcessor processor = new SimpleRequestProcessor(
			createServiceLayer());
	private static EntityManager entityManagerMock = mock(EntityManager.class);

	/**
	 * Returns a fully initialized {@link RequestFactory} of the given type,
	 * that can be used to emit one or more requests to the server.
	 */
	public static <T extends RequestFactory> T createFactory(Class<T> clazz) {
		T factory = RequestFactorySource.create(clazz);
		factory.initialize(new SimpleEventBus(), new InProcessRequestTransport(
				processor));
		return factory;
	}

	/**
	 * Always use this method to retrieve an instance of a service that is
	 * called by the GWT locator. As this instance is cached by GWT,
	 * {@link Mockito#reset(Object...)} is called on the instance to avoid side
	 * effects with other tests.
	 */
	public static <T> T getServiceMock(Class<T> clazz) {
		T service = clazz.cast(mockingServiceLocator.getInstance(clazz));
		reset(service);
		return service;
	}

	public static EntityManager getEntityManagerMock() {
		reset(entityManagerMock);
		return entityManagerMock;
	}

	public static <T extends RequestContext> Provider<T> createRequestContextProvider(
			Class<? extends RequestFactory> requestFactoryClass) {
		RequestFactory requestFactory = createFactory(requestFactoryClass);
		return createRequestContextProvider(requestFactory);
	}

	public static <T> Provider<T> createRequestContextProvider(
			final RequestFactory requestFactory) {
		Provider<T> provider = mock(Provider.class);
		when(provider.get()).thenAnswer(new Answer<Object>() {
			@Override
			public Object answer(InvocationOnMock invocation) throws Throwable {
				Method method = ReflectionUtils.findMethod(
						requestFactory.getClass(), "context");
				return ReflectionUtils.invokeMethod(method, requestFactory);
			}
		});
		return provider;
	}

	public static <T> T fireRequest(Request<T> request) {
		Receiver<T> receiver = mock(Receiver.class);
		request.fire(receiver);
		return CaptureUtils.capture(receiver);
	}

	private static ServiceLayer createServiceLayer() {
		return ServiceLayer.create(new MockedServiceLayer());
	}
}
